/*
 * Copyright (c) 2013-2015 Charkey. All rights reserved.
 *
 * This software is the confidential and proprietary information of Charkey.
 * You shall not disclose such Confidential Information and shall use it only
 * in accordance with the terms of the agreements you entered into with Charkey.
 *
 * Charkey MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE SOFTWARE,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT.
 *
 * Charkey SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING,
 * MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
 */

package cn.simastudio.talos.core.controller;

import cn.simastudio.talos.common.constants.ApiStatusCode;
import cn.simastudio.talos.common.constants.SimaConstants;
import cn.simastudio.talos.common.model.JsonResult;
import cn.simastudio.talos.core.authc.token.OAuth2Token;
import cn.simastudio.talos.core.service.base.OAuthService;
import com.alibaba.fastjson.JSON;
import org.apache.oltu.oauth2.as.issuer.MD5Generator;
import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl;
import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest;
import org.apache.oltu.oauth2.as.response.OAuthASResponse;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.error.OAuthError;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.OAuthResponse;
import org.apache.oltu.oauth2.common.message.types.ResponseType;
import org.apache.oltu.oauth2.common.utils.OAuthUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * 授权控制器
 * 1、首先通过如http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&
 * response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login访问授权页面；
 * 2、该控制器首先检查clientId是否正确；如果错误将返回相应的错误信息；
 * 3、然后判断用户是否登录了，如果没有登录首先到登录页面登录；
 * 4、登录成功后生成相应的auth code即授权码，然后重定向到客户端地址，如http://localhost:9080/chapter17-client/oauth2-login?code=52b1832f5dff68122f4f00ae995da0ed；
 * 在重定向到的地址中会带上code参数（授权码），接着客户端可以根据授权码去换取access token。
 */
@Controller
public class AuthorizeController {

    private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizeController.class);

    @Autowired
    private OAuthService oAuthService;

    /**
     * OAuth2.0 用户登录授权
     *
     * @param model   SpringMVC Model
     * @param request HttpServletRequest
     * @return 根据登录结果返回不同的结果
     * @throws URISyntaxException
     * @throws OAuthSystemException
     */
    @SuppressWarnings("unchecked")
    @RequestMapping("/oauth2/authorize")
    public Object authorize(Model model, HttpServletRequest request)
            throws URISyntaxException, OAuthSystemException, ServletException, IOException {

        // OAuth 2.0 Authorization Server: https://cwiki.apache.org/confluence/display/OLTU/OAuth+2.0+Authorization+Server

        // 获取请求参数中的request_for，如果参数值为page，则在未授权时返回登录页；如参数值为json，则在未授权时返回描述错误的json
        String requestFor = request.getParameter(SimaConstants.Auth.REQUEST_FOR);

        try {
            // 构建 OAuth 授权请求
            OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);

            // 检查传入的客户端id client_id 是否正确
            if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
                LOGGER.debug("invalid client_id/client_secret");
                // client_id不正确的话做如下响应
                OAuthResponse oAuthResponse =
                        OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                                .setError(OAuthError.TokenResponse.INVALID_CLIENT)
                                .setErrorDescription("invalid client_id/client_secret")
                                .setParam(SimaConstants.Response.CODE, ApiStatusCode.INVALID_CLIENT)
                                .buildJSONMessage();
                return new ResponseEntity(oAuthResponse.getBody(), HttpStatus.valueOf(oAuthResponse.getResponseStatus()));
            }

            Subject subject = SecurityUtils.getSubject();

            // 如果用户没有登录，跳转到登录页面
            //if (!subject.isAuthenticated()) {
            // 登录校验
            if (!login(subject, request)) {
                if (SimaConstants.Auth.REQUEST_FOR_PAGE.equals(requestFor) || "null".equals(requestFor)) {
                    //登录失败时跳转到登录页面
                    model.addAttribute(SimaConstants.Response.CODE, request.getAttribute(SimaConstants.Response.MESSAGE));
                    return "/jsp/oauth2/login.jsp";
                } else if (SimaConstants.Auth.REQUEST_FOR_JSON.equals(requestFor) || !("").equals(requestFor)) {
                    OAuthResponse oAuthResponse =
                            OAuthASResponse.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
                                    .setError(OAuthError.CodeResponse.ACCESS_DENIED)
                                    .setErrorDescription(String.valueOf(request.getAttribute(SimaConstants.Response.MESSAGE)))
                                    .setParam(SimaConstants.Response.CODE, ApiStatusCode.UNAUTHORIZED)
                                    .buildJSONMessage();
                    return new ResponseEntity(oAuthResponse.getBody(), HttpStatus.valueOf(oAuthResponse.getResponseStatus()));
                }
            }
            //}

            // OAuth2 登录的用户名
            String credential = (String) subject.getPrincipal();
            // 生成授权码
            String authorizationCode = null;
            // responseType 目前仅支持 CODE，另外还有 TOKEN

            String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);
            if (responseType.equals(ResponseType.CODE.toString())) {
                OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
                authorizationCode = oauthIssuerImpl.authorizationCode();
                // 将 auth_code 放到 auth_code_cache 中，过期时间 10分钟
                oAuthService.addAuthCode(authorizationCode, credential);
            }

            if (SimaConstants.Auth.REQUEST_FOR_PAGE.equals(requestFor) || "null".equals(requestFor)) {
                // 进行 OAuth 响应构建
                OAuthASResponse.OAuthAuthorizationResponseBuilder builder =
                        OAuthASResponse.authorizationResponse(request, HttpServletResponse.SC_FOUND);
                // 设置授权码
                builder.setCode(authorizationCode);

                // 得到到客户端重定向地址
                String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);

                // 构建响应：设置回调地址、过期时间
                final OAuthResponse oAuthResponse = builder.location(redirectURI).setExpiresIn(oAuthService.getAuthCodeExpireIn()).buildQueryMessage();

                // 根据 OAuthResponse 返回 ResponseEntity 响应
                HttpHeaders headers = new HttpHeaders();
                headers.setLocation(new URI(oAuthResponse.getLocationUri()));
                return new ResponseEntity(headers, HttpStatus.valueOf(oAuthResponse.getResponseStatus()));
            } else if (SimaConstants.Auth.REQUEST_FOR_JSON.equals(requestFor) || !("").equals(requestFor)) {
                JsonResult result = new JsonResult();
                result.setCode(ApiStatusCode.OK);
                result.setData(authorizationCode);
                // the following code will cause java.lang.IllegalStateException: getInputStream() has already been called for this request
                //return request;
                return JSON.toJSON(result);
            }
            return null;
        } catch (OAuthProblemException e) {
            LOGGER.debug("OAuthProblemException, error occurred!");
            // 错误：redirect_uri 为空
            if (OAuthUtils.isEmpty(request.getParameter(OAuth.OAUTH_REDIRECT_URI))) {
                OAuthResponse oAuthResponse =
                        OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                                .setError(OAuthError.TokenResponse.INVALID_REQUEST)
                                .setErrorDescription("OAuth callback url (parameter name: redirect_uri) needs to be provided by client!")
                                .setParam(SimaConstants.Response.CODE, ApiStatusCode.INVALID_CALLBACK_URL)
                                .buildJSONMessage();
                return new ResponseEntity(oAuthResponse.getBody(), HttpStatus.valueOf(oAuthResponse.getResponseStatus()));
            }
            // 错误：response_type 为空
            if (OAuthUtils.isEmpty(request.getParameter(OAuth.OAUTH_RESPONSE_TYPE))) {
                OAuthResponse oAuthResponse =
                        OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                                .setError(OAuthError.TokenResponse.INVALID_REQUEST)
                                .setErrorDescription("OAuth response type (parameter name: response_type) needs to be provided by client!")
                                .setParam(SimaConstants.Response.CODE, ApiStatusCode.INVALID_RESPONSE_TYPE)
                                .buildJSONMessage();
                return new ResponseEntity(oAuthResponse.getBody(), HttpStatus.valueOf(oAuthResponse.getResponseStatus()));
            }
            // 错误：client_id 为空
            if (OAuthUtils.isEmpty(request.getParameter(OAuth.OAUTH_CLIENT_ID))) {
                OAuthResponse oAuthResponse =
                        OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                                .setError(OAuthError.TokenResponse.INVALID_REQUEST)
                                .setErrorDescription("OAuth client id (parameter name: client_id) needs to be provided by client!")
                                .setParam(SimaConstants.Response.CODE, ApiStatusCode.INVALID_CLIENT)
                                .buildJSONMessage();
                return new ResponseEntity(oAuthResponse.getBody(), HttpStatus.valueOf(oAuthResponse.getResponseStatus()));
            }
            // 错误：state 为空
            if (OAuthUtils.isEmpty(request.getParameter(OAuth.OAUTH_STATE))) {
                OAuthResponse oAuthResponse =
                        OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                                .setError(OAuthError.TokenResponse.INVALID_REQUEST)
                                .setErrorDescription("OAuth state (parameter name: state) needs to be provided by client!")
                                .setParam(SimaConstants.Response.CODE, ApiStatusCode.INVALID_STATE)
                                .buildJSONMessage();
                return new ResponseEntity(oAuthResponse.getBody(), HttpStatus.valueOf(oAuthResponse.getResponseStatus()));
            }
            // 返回错误消息（如?error=）
            final OAuthResponse oAuthResponse =
                    OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND)
                            .error(e).location(e.getRedirectUri()).buildQueryMessage();
            HttpHeaders headers = new HttpHeaders();
            headers.setLocation(new URI(oAuthResponse.getLocationUri()));
            return new ResponseEntity(headers, HttpStatus.valueOf(oAuthResponse.getResponseStatus()));
        }
    }

    /**
     * OAuth2.0 登录操作
     *
     * @param subject 登录主体
     * @param request HttpServletRequest请求
     * @return true：登录成功；false：登录失败
     */
    private boolean login(Subject subject, HttpServletRequest request) {
        // POST 请求才执行登录操作
        if (OAuth.HttpMethod.GET.equalsIgnoreCase(request.getMethod())) {
            return false;
        }

        // 获得登录的用户名密码（用户名如 username、email、tel 等，依项目而定）
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        // 用户名或密码为空，直接登录失败
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            return false;
        }

        // 创建自定义的 OAuth2Token，用于 OAuth2Realm 验证
        OAuth2Token token = new OAuth2Token(username, password);

        try {
            subject.login(token);
            return true;
        } catch (AuthenticationException e) {
            request.setAttribute(SimaConstants.Response.MESSAGE, "AuthenticationException");
            return false;
        }
    }
}